﻿// 支持 Cookie/URL 鉴权的 SSE 管理器（可选 fetch 流自定义请求头）
// ---------------------------------------------------------------
// 环境变量（按需配置）：
// VITE_SSE_BASE=/support-backend/topic      // ★ 必填：基路径（网关前缀 + /topic）
// VITE_SSE_TRANSPORT=auto|eventsource|fetch // 选填：默认 auto；需要自定义头就设 fetch
// VITE_SSE_WITH_CREDENTIALS=true|false      // 选填：Cookie 鉴权设 true
// VITE_SSE_TOKEN_PARAM=access_token         // 选填：URL 鉴权时的参数名（自动拼 localStorage.token）
//
// 使用方式见 registerMessageHandlers.js
// ---------------------------------------------------------------

const SSE_BASE = import.meta.env.VITE_SSE_BASE ?? "/topic";
const RECONNECT_BASE = Number(import.meta.env.VITE_SSE_RECONNECT_BASE || 3000);
const RECONNECT_MAX = Number(import.meta.env.VITE_SSE_RECONNECT_MAX || 60000);
const WITH_CREDENTIALS = import.meta.env.VITE_SSE_WITH_CREDENTIALS === "true";
const TOKEN_PARAM = (import.meta.env.VITE_SSE_TOKEN_PARAM || "").trim();
const TRANSPORT = (import.meta.env.VITE_SSE_TRANSPORT || "auto").toLowerCase(); // auto|eventsource|fetch
// 放在文件顶部工具区
function isHeartbeat(payload) {
  // 字符串心跳：": heartbeat" / "heartbeat" / ":heartbeat" / "ping" / "pong" / "keepalive"
  if (typeof payload === "string") {
    const t = payload.trim();
    if (!t) return true; // 空串也当成心跳/无效块直接忽略
    return /^(?::\s*)?(heartbeat|ping|pong|keepalive)$/i.test(t);
  }
  // 对象心跳：{ type: 'heartbeat' } 或 { event: 'ping' }
  if (payload && typeof payload === "object") {
    const v = String(payload.type || payload.event || "").toLowerCase();
    if (["heartbeat", "ping", "pong", "keepalive"].includes(v)) return true;
  }
  return false;
}
function getToken() {
  return localStorage.getItem("token") || "";
}
function safeParse(s) {
  try {
    return JSON.parse(s);
  } catch {
    return s;
  }
}
function join(base, sub) {
  const a = base.endsWith("/") ? base.slice(0, -1) : base;
  const b = String(sub || "");
  return b.startsWith("/") ? a + b : a + "/" + b;
}
function buildUrl(subPath) {
  const u = new URL(join(SSE_BASE, String(subPath).replace(/^\/+/, "")), window.location.origin);
  const tk = getToken();
  if (TOKEN_PARAM && tk) u.searchParams.set(TOKEN_PARAM, tk);
  return u.toString();
}

export default class SseCenter {
  /**
   * @param {{debug?: boolean, buildHeaders?: ()=>Record<string,string>}} opts
   *  - buildHeaders：只有在 VITE_SSE_TRANSPORT=fetch 下才会生效，用于自定义请求头（如 Authorization）
   */
  constructor(opts = {}) {
    this.debug = !!opts.debug;
    this.buildHeaders = typeof opts.buildHeaders === "function" ? opts.buildHeaders : () => ({});
    this.started = false;

    this.handlers = new Map(); // topic -> handler
    this.sources = new Map(); // topic -> (EventSource|AbortController)
    this.reconn = new Map(); // topic -> {timer, backoff}

    this._bindVisibility();
    this._bindNetwork();
  }

  // ------------- EventSource 路线（不支持自定义请求头） -------------
  _openES(topic, url) {
    this._close(topic);
    const es = new EventSource(url, { withCredentials: WITH_CREDENTIALS });
    es.onopen = () => {
      this.debug && console.log("[SSE] open", topic, url, "(EventSource)");
      this._resetBackoff(topic);
    };
    es.onmessage = ev => {
      const raw = String(ev?.data ?? "");
      // 过滤心跳（例如服务端发的 "data:: heartbeat" => raw === ": heartbeat"）
      if (isHeartbeat(raw)) return;
      const data = safeParse(raw);
      if (isHeartbeat(data)) return; // 兼容 JSON 结构心跳
      try {
        this.handlers.get(topic)?.(data, ev);
        window.dispatchEvent(new CustomEvent("message:announcement", { detail: { topic, data } }));
      } catch (e) {
        console.error(e);
      }
    };
    es.onerror = err => {
      this.debug && console.warn("[SSE] error", topic, err);
      this._scheduleReconnect(topic);
    };
    this.sources.set(topic, es);
  }

  // ------------- fetch 流路线（可自定义请求头） -------------
  async _openFetch(topic, url) {
    this._close(topic);
    const ctrl = new AbortController();
    this.sources.set(topic, ctrl);

    const headers = new Headers({ Accept: "text/event-stream", "Cache-Control": "no-cache" });
    const extra = this.buildHeaders() || {};
    for (const [k, v] of Object.entries(extra)) if (k && v != null) headers.set(k, String(v));

    try {
      const resp = await fetch(url, {
        method: "GET",
        mode: "cors",
        credentials: WITH_CREDENTIALS ? "include" : "same-origin",
        headers,
        signal: ctrl.signal,
      });
      if (!resp.ok) throw new Error(`SSE HTTP ${resp.status}`);
      this.debug && console.log("[SSE] open", topic, url, "(fetch)");
      this._resetBackoff(topic);

      const reader = resp.body.getReader();
      const decoder = new TextDecoder();
      let buffer = "";

      const pump = async () => {
        const { value, done } = await reader.read();
        if (done) throw new Error("SSE stream closed");
        buffer += decoder.decode(value, { stream: true });

        // text/event-stream：以空行分块；块内行形如 "data: xxx"
        const chunks = buffer.split(/\r?\n\r?\n/);
        buffer = chunks.pop() || "";
        for (const chunk of chunks) {
          let dataStr = "";
          for (const line of chunk.split(/\r?\n/)) {
            if (line.startsWith("data:")) dataStr += line.slice(5).trimStart() + "\n";
            // 如需自定义 event，可在这解析 "event:" 行并分发
          }
          if (dataStr) {
            const payload = safeParse(dataStr.replace(/\n$/, ""));
            try {
              this.handlers.get(topic)?.(payload, null);
              window.dispatchEvent(new CustomEvent("message:announcement", { detail: { topic, data: payload } }));
            } catch (e) {
              console.error(e);
            }
          }
        }
        await pump();
      };

      pump().catch(e => {
        this.debug && console.warn("[SSE] fetch stream end", topic, e?.message);
        this._scheduleReconnect(topic);
      });
    } catch (e) {
      this.debug && console.warn("[SSE] fetch open fail", topic, e?.message);
      this._scheduleReconnect(topic);
    }
  }

  _open(topic) {
    const url = buildUrl(topic);
    const useFetch = TRANSPORT === "fetch" || (TRANSPORT === "auto" && typeof EventSource === "undefined");
    this.debug && console.log("[SSE] OPEN", topic, url, "| transport:", useFetch ? "fetch" : "EventSource");
    if (useFetch) this._openFetch(topic, url);
    else this._openES(topic, url);
  }

  _close(topic) {
    const s = this.sources.get(topic);
    if (!s) return;
    try {
      if (typeof s.close === "function") s.close(); // EventSource
      if (typeof s.abort === "function") s.abort(); // AbortController
    } catch (e) {
      console.error("[SSE] close error", topic, e);
    }
    this.sources.delete(topic);
  }

  _nextBackoff(topic) {
    const o = this.reconn.get(topic) || { backoff: 0, timer: null };
    o.backoff = Math.min(o.backoff ? o.backoff * 2 : RECONNECT_BASE, RECONNECT_MAX);
    this.reconn.set(topic, o);
    return o.backoff;
  }
  _resetBackoff(topic) {
    const o = this.reconn.get(topic) || { backoff: 0, timer: null };
    if (o.timer) clearTimeout(o.timer);
    o.backoff = 0;
    o.timer = null;
    this.reconn.set(topic, o);
  }
  _scheduleReconnect(topic) {
    const o = this.reconn.get(topic) || { backoff: 0, timer: null };
    if (o.timer) return;
    const delay = this._nextBackoff(topic);
    this.debug && console.log("[SSE] reconnect in", delay, "ms for", topic);
    o.timer = setTimeout(() => {
      o.timer = null;
      if (this.started) this._open(topic);
    }, delay);
    this.reconn.set(topic, o);
    this._close(topic); // 避免与浏览器内置重连“打架”
  }
  _clearAllReconnects() {
    for (const o of this.reconn.values()) o?.timer && clearTimeout(o.timer);
    this.reconn.clear();
  }

  _bindVisibility() {
    if (typeof document === "undefined") return;
    document.addEventListener("visibilitychange", () => {
      if (document.visibilityState === "visible" && this.started) {
        for (const t of this.handlers.keys()) this._open(t);
      }
    });
  }
  _bindNetwork() {
    if (typeof window === "undefined") return;
    window.addEventListener("online", () => {
      if (this.started) for (const t of this.handlers.keys()) this._open(t);
    });
    window.addEventListener("offline", () => {
      for (const t of this.sources.keys()) this._close(t);
    });
  }

  // ---------------- 对外 API ----------------
  start() {
    this.started = true;
    for (const t of this.handlers.keys()) this._open(t);
  }
  stop() {
    this.started = false;
    for (const t of this.sources.keys()) this._close(t);
    this._clearAllReconnects();
  }
  subscribe(topic, handler) {
    const t = String(topic).replace(/^\/+/, "");
    this.handlers.set(t, handler);
    if (this.started) this._open(t);
    return () => this.unsubscribe(t);
  }
  unsubscribe(topic) {
    const t = String(topic).replace(/^\/+/, "");
    this.handlers.delete(t);
    this._close(t);
    const o = this.reconn.get(t);
    if (o?.timer) clearTimeout(o.timer);
    this.reconn.delete(t);
  }
  publish() {
    return false;
  } // SSE 为下行-only，保留接口一致性
}
